﻿using System;
using System.Collections.Generic;
using OpenTap.Plugins.Interfaces.Common;
using OpenTap.Plugins.Interfaces.PM;

namespace OpenTap.Plugins.Pm
{
    public abstract class PmBase : ScpiInstrument, IPm
    {
        /// <summary>
        ///     This enum is used to specify data format with method: SetDataFormat().
        /// </summary>
        protected enum EDataFormat
        {
            Ascii,
            Real
        }

        /// <summary>
        ///     This enum is used to scpecify measurement speed with method: SetMeasurementSpeed().
        /// </summary>
        protected enum EMeasurementSpeed
        {
            Normal,
            Double,
            Fast
        }

        /// <summary>
        ///     This enum is used to specify trigger source with method: SetTrigSource().
        /// </summary>
        protected enum ETrigSource
        {
            Bus, External, Hold, Immediate, Internal
        }

        protected long MinFrequencyHz;
        protected long MaxFrequencyHz;
        protected double TimeOutOpcTypicSec = 2;    // Typical timeout with Operation complete method.
        protected double MeasSpeed;                 // Used to timeout calculation with measurements

        /// <inheritdoc />
        public abstract void ExecuteZeroCompensation();

        /// <inheritdoc />
        public abstract double MeasurePower(long frequencyKHz);

        /// <summary>
        ///     Checks from instrument, if previous operation is completed or not.
        ///     Waits for a all previously executed SCPI commands to complete.
        /// </summary>
        /// <param name="timeOutSec">Maximum time to wait, default is 2 seconds. If value is less than IoTimeout, IoTimeout is used.</param>
        /// <returns>Operation complete status</returns>
        protected virtual EopcStatus Opc(double timeOutSec)
        {
            DateTime timeStampStart = DateTime.Now;

            WaitForOperationComplete(Convert.ToInt32(timeOutSec * 1000)); // Par: Maximum time to wait.
                    // If value is less than IoTimeout, IoTimeout will be used.

            TimeSpan timeUsedDateForm = DateTime.Now - timeStampStart;
            var timeUsedNumbSec = Convert.ToSingle(timeUsedDateForm.TotalSeconds);
            if (timeUsedNumbSec > 0.66 * timeOutSec && timeUsedNumbSec > 0.66 * IoTimeout / 1000.0)
                Log.Debug("WaitForOperationComplete UsedSec( " + timeUsedNumbSec + " / " + (timeOutSec > IoTimeout / 1000.0 ? timeOutSec : IoTimeout / 1000.0) + " )");

            if (timeUsedNumbSec < timeOutSec || timeUsedNumbSec < IoTimeout / 1000.0)
                return EopcStatus.Complete;

            // Timeout occurred
            Log.Debug("scpiInstrument (IoTimeout Sec) = " + IoTimeout / 1000);
            Log.Debug("PM:Opc.WaitForOperationComplete(timeOutSec) = " + timeOutSec);
            Log.Debug("Timeout(" + timeUsedNumbSec + ") occurred, when WaitForOperationComplete!");
            throw new TimeoutException("Timeout(" + timeUsedNumbSec + ") occurred, when WaitForOperationComplete! \nIoTimeout in sec.= " + IoTimeout / 1000 + " \nPM:Opc(timeOutSec) = " + timeOutSec);
        }

        /// <summary>
        ///     Querys ERRors. Throws an exception, if error occurred and exception wanted by par.
        /// </summary>
        /// <param name="ifErrRunException">If true, run Exception with error. False can be used to clean error queue to log without exception.</param>
        /// <param name="infoMessToShow">Info message to be shown with exception. E.G. "Method name" + information</param>
        /// <returns>Error code of last error</returns>
        /// <exception cref="InvalidOperationException">Throws exception with info message, if error occurred.</exception>
        protected int QueryErrWithExc(bool ifErrRunException, string infoMessToShow)
        {
            int retResult = -1; // Result initialized to error-state
            var errors = QueryErrors(true, 100);

            if (errors.Count > 0)
            {
                string errCollection = "";
                Log.Info("Error count: " + errors.Count + " with: " + infoMessToShow);
                for (int i = 0; i < (errors.Count); i++)
                {
                    retResult = errors[i].Code;
                    Log.Info("ERRor[" + errors[i].Code + "] " + errors[i].Message);
                    errCollection = String.Concat(errCollection, ", \n", errors[i].Code, " ", errors[i].Message);
                }

                if (ifErrRunException)
                {
                    if (errors.Count == 1)
                        throw new InvalidOperationException("ERRor occurred with " + infoMessToShow + " as follow" + errCollection);
                    throw new InvalidOperationException("ERRors occurred with " + infoMessToShow + " as follow" + errCollection);
                }
            }else
                retResult = 0;

            return (retResult);
        }

        /// <inheritdoc />
        public new virtual List<ScpiError> QueryErrors(bool suppressLogMessages, int maxErrors)
        {
            return base.QueryErrors(suppressLogMessages, maxErrors);
        }

        public void Clear()
        {
            ScpiCommand("*CLS");
        }

        /// <summary>
        ///     Performs the self-test.
        /// </summary>
        /// <returns>Result: False=Pass / True=Fail</returns>
        protected bool SelfTest()
        {
            var response = ScpiQuery("*TST?");
            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "Selftest: *TST?");

            if (response == "+0\n" || response == "0\n")
            {
                Log.Info("PM:Selftest PASS");
                return false;       // PASS
            }

            Log.Info("PM:Selftest FAIL");
            return true;       // FAIL
        }

        /// <inheritdoc />
        public virtual void SetAverageAuto()
        {
            ScpiCommand("SENS:DET:FUNC AVER");          // Measurement mode to average.
            ScpiCommand("SENS:AVER:COUN:AUTO ON");      // Enables automatic filter length selection.
            // ScpiCommand("SENS:AVER:STAT ON");NO NEED, because of upper command "ON" => Sets
            // automatically [SENSe[1]:]AVERage:STATe command to ON. #ProgGuide p.270

            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "PM:SetAverageAuto()");
        }

        /// <inheritdoc />
        public virtual void SetAverageOff()
        {
            ScpiCommand("SENS:DET:FUNC NORM");          // Measurement mode to NORMal.
            ScpiCommand("SENS:AVER:STAT OFF");          // Disables averaging.
            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "PM:SetAverageOff()");
        }

        /// <inheritdoc />
        public virtual void SetAverageManual(int averageCount)
        {
            if (averageCount < 1)
                throw new ArgumentException("PM:SetAverageManual Invalid parameter value(" + averageCount + ")", "averageCount");

            ScpiCommand("SENS:DET:FUNC AVER");          // Measurement mode to average.
            ScpiCommand("SENS:AVER:COUN:AUTO OFF");
            ScpiCommand("SENS:AVER:COUN " + averageCount);
            // ScpiCommand("SENS:AVER:STAT ON"); NO NEED, because of entering value to upper command => Sets
            // automatically [SENSe[1]:]AVERage:STATe command to ON. #ProgGuide p.268

            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "PM:SetAverageManual(" + averageCount + ")");
        }

        public virtual void ActivateNsRatioAutoAveraging(double maxMeasTimeS, double nsRatio)
        {
            throw new NotImplementedException();
        }

        //protected void SetDataFormat(EDataFormat format)
        //{
        //    switch (format)
        //    {
        //        case EDataFormat.Ascii:
        //            ScpiCommand("FORM ASC");
        //            break;

        //        case EDataFormat.Real:
        //            ScpiCommand("FORM REAL");
        //            break;

        //        default:
        //            throw new ArgumentException("PM:SetDataFormat Invalid parameter value(" + format + ")", "format");
        //    }

        //    Opc(TimeOutOpcTypicSec);
        //    QueryErrWithExc(true, "SetDataFormat(" + format + ")");
        //}

        /// <summary>
        ///     Sets the measurement speed.
        /// </summary>
        /// <param name="speed">Normal = 20 readings/s, Double = 40 readings/s or Fast = 3500 readings/s</param>
        protected void SetMeasurementSpeed(EMeasurementSpeed speed)
        {
            switch (speed)
            {
                case EMeasurementSpeed.Normal:
                    ScpiCommand("SENS:MRAT NORM");
                    MeasSpeed = 20;

                    break;

                case EMeasurementSpeed.Double:
                    ScpiCommand("SENS:MRAT DOUB");
                    MeasSpeed = 40;
                    break;

                case EMeasurementSpeed.Fast:
                    ScpiCommand("SENS:MRAT FAST");
                    MeasSpeed = 3500;
                    break;

                default:
                    throw new ArgumentException("PM:SetMeasurementSpeed Invalid parameter value(" + speed + ")", "speed");
            }

            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "PM:SetMeasurementSpeed:(" + speed + ")");

        }

        /// <summary>
        ///     Defines a fixed offset in dB, which is used to correct the measured value.
        ///     This can be configured to compensate for signal loss or gain in your test setup.
        ///     (for example, to compensate for the loss of a 10 dB attenuator).
        /// </summary>
        /// <param name="state">Defines if offset is in use or not.</param>
        /// <param name="offsetDb">The offset value in dB [–100 to +100 dB].</param>
        public virtual void SetOffSetValue(EState state, double offsetDb)
        {
            switch (state)
            {
                case EState.On:
                    // if (-100 > offsetDb || 100 < offsetDb) // Limit check removed, because of different equipment possible
                    // have different limits and QueryErrWithExc handle this succesfully
                    ScpiCommand("SENS:CORR:GAIN2 " + offsetDb);
                    // ScpiCommand("SENS:CORR:GAIN2:STAT ON");  If an offset value entered, this state is automatically enabled. #Programming Guide / Chapter: Setting offsets / Page: 38
                    break;

                case EState.Off:
                    ScpiCommand("SENS:CORR:GAIN2:STAT OFF");
                    break;

                default:
                    throw new ArgumentException("PM:SetOffSetValue Invalid parameter value(" + state + ")", "state");
            }

            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "PM:SetOffSetValue(" + state + ", " + offsetDb + ")");
        }

        /// <summary>
        ///     Enables and disables step detection. In the AUTO filter mode, the average of the last four
        ///     values entered into the filter is compared to the average of the entire filter.
        ///     If the difference between the two averages is greater than 12.5%, the digital filter is cleared.
        /// </summary>
        /// <param name="state">Off, On</param>
        protected void SetStepDetectionState(EState state)
        {
            switch (state)
            {
                case EState.On:
                    ScpiCommand("SENS:AVER:SDET ON");
                    break;

                case EState.Off:
                    ScpiCommand("SENS:AVER:SDET OFF");
                    break;

                default:
                    throw new ArgumentException("PM:SetStepDetectionState Invalid parameter value(" + state + ")", "state");
            }

            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "SetStepDetectionState(" + state + ")");
        }

        /// <summary>
        ///     This command configures the trigger system to respond to the specified source.
        ///     This command only selects the trigger source. Use the INITiate command to place
        ///     the power sensor in the wait-for-trigger state.
        /// </summary>
        /// <param name="source">Bus: the trigger source is the group execute trigger. Get bus command, a *TRG common command, or the TRIGger:IMMediate SCPI command.
        ///     External: the trigger source is the external trigger input.
        ///     Hold: triggering is suspended. The only way to trigger the U2020 X-Series is to use TRIGger:IMMediate.
        ///     Immediate(Default): the trigger system is always true. If INITiate:CONTinuous is ON, the power sensor is in free run mode. If an INITiate:IMMediate command is sent, a measurement is triggered then the sensor returns to the idle state.
        ///     Internal: the trigger source is Channel A.</param>
        protected void SetTrigSource(ETrigSource source)
        {
            switch (source)
            {
                case ETrigSource.Bus:
                    ScpiCommand("TRIG:SOUR BUS");
                    break;

                case ETrigSource.External:
                    ScpiCommand("TRIG:SOUR EXT");
                    break;

                case ETrigSource.Hold:
                    ScpiCommand("TRIG:SOUR HOLD");
                    break;

                case ETrigSource.Immediate:
                    ScpiCommand("TRIG:SOUR IMM");
                    break;

                case ETrigSource.Internal:
                    ScpiCommand("TRIG:SOUR INT");
                    break;

                default:
                    throw new ArgumentException("PM:SetTrigSource Invalid parameter value(" + source + ")", "source");
            }

            Opc(TimeOutOpcTypicSec);
            QueryErrWithExc(true, "PM:SetTrigSource(" + source + ")");
        }

        public abstract double MeasureBurstPower(long frequencyKHz);

        public new virtual void WaitForOperationComplete(int timeoutMs = 2000)
        {
            base.WaitForOperationComplete(timeoutMs);
        }
    }
}
